利用 Windows 内建的 Driver 透過 IOCTL 發送 NVMe Command


来自 <https://zhung.com.tw/article/%E5%88%A9%E7%94%A8windows%E5%86%85%E5%BB%BA%E7%9A%84driver%E9%80%8F%E9%81%8Eioctl%E7%99%BC%E9%80%81nvme-command/> 


Limitations

劈頭就要講限制因為 Windows 就是麻煩,不像在 Linux 上可以直接使用 linux-nvme/nvme-cli 就搞定。限制就是只有特定幾個 NVMe Admin Command 可以下,網路上的 VS Studio 專案可以直接拿來用,也有表格註明能用的 Command 們。但人生就是這個但是,我編譯不過啊@@ 只好自己寫再編譯,並且就有了本篇。

NVMe IO Command – NVM Command Set 的部分大都可以透過一對一的 SCSI Command [2] 來做到,因此以下主要講 Admin Command。

Sample Codes

我知道大家都只要這個。

For Generic Commands

#include <windows.h>

#include "nvme.h" //remember to include this

typedef VOID (*CQ_CALLBACK)(DWORD req_val, DWORD cdw0);

/*!

 * For Identify, Get Feature, Get Log Page Only

 * @param data_type can only be NVMeDataTypeIdentify, NVMeDataTypeFeature, or NVMeDataTypeLogPage

 * @param req_val request value means CNS, FID, or LID

 * @param req_sub_val

 * @param pdata used if additional data is request

 * @param xfer_len size of pdata in bytes

 * @param callback a void function takes 2 parameters: void (req_val, cdw0)

 */

DWORD nvme_specific(HANDLE FileHandle, STORAGE_PROTOCOL_NVME_DATA_TYPE data_type, DWORD req_val, DWORD req_sub_val, PVOID pdata, DWORD xfer_len, CQ_CALLBACK callback)

{

    BOOL result;

    PVOID buffer = NULL;

    ULONG bufferLength = 0;

    ULONG returnedLength = 0;

PSTORAGE_PROPERTY_QUERY query = NULL;

    PSTORAGE_PROTOCOL_SPECIFIC_DATA protocolData = NULL;

    PSTORAGE_PROTOCOL_DATA_DESCRIPTOR protocolDataDescr = NULL;

//

    // Allocate buffer for use.

    //

    bufferLength = FIELD_OFFSET(STORAGE_PROPERTY_QUERY, AdditionalParameters) + sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + xfer_len;

    buffer = malloc(bufferLength);

if (buffer == NULL)

    {

        printf("DeviceNVMeQueryProtocolDataTest: allocate buffer failed, exit.\n");

        return 1;

    }

//

    // Initialize query data structure to get Identify Controller Data.

    //

    ZeroMemory(buffer, bufferLength);

query = (PSTORAGE_PROPERTY_QUERY)buffer;

    protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;

    protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;

query->PropertyId = StorageAdapterProtocolSpecificProperty;

    query->QueryType = PropertyStandardQuery;

protocolData->ProtocolType = ProtocolTypeNvme;

    protocolData->DataType = data_type;

    protocolData->ProtocolDataRequestValue = req_val;

    protocolData->ProtocolDataRequestSubValue = req_sub_val;

    if (xfer_len)

    {

        protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);

        protocolData->ProtocolDataLength = xfer_len;

    }

//

    // Send request down.

    //

    result = DeviceIoControl(FileHandle,

                             IOCTL_STORAGE_QUERY_PROPERTY,

                             buffer,

                             bufferLength,

                             buffer,

                             bufferLength,

                             &returnedLength,

                             NULL);

if (!result || (returnedLength == 0))

    {

        printf("FAIL, Error Code=%d\n", GetLastError());

        return GetLastError();

    }

//

    // Validate the returned data.

    //

    if ((protocolDataDescr->Version != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR)) ||

        (protocolDataDescr->Size != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR)))

    {

        printf("Data descriptor header not valid\n");

        return 1;

    }

protocolData = &protocolDataDescr->ProtocolSpecificData;

    memcpy_s(pdata, xfer_len, (PCHAR)protocolData + protocolData->ProtocolDataOffset, xfer_len);

if (callback != NULL)

    {

        callback(req_val, protocolDataDescr->ProtocolSpecificData.FixedProtocolReturnData);

    }

free(buffer);

    return 0;

}

然後就可以下個 Identify 之類:

DWORD nvme_identify()

{

    HANDLE FileHandle = CreateFileA(

        "\\\\.\\physicaldrive0", GENERIC_WRITE | GENERIC_READ,

    FILE_SHARE_READ | FILE_SHARE_WRITE,

    NULL, OPEN_EXISTING, 0, NULL

  );

    CHAR pdata[NVME_MAX_LOG_SIZE];

    if (nvme_specific(FileHandle, NVMeDataTypeIdentify, NVME_IDENTIFY_CNS_CONTROLLER, 0, pdata, NVME_MAX_LOG_SIZE, NULL)) {

        return 1;

    }

PNVME_IDENTIFY_CONTROLLER_DATA identifyControllerData = (PNVME_IDENTIFY_CONTROLLER_DATA)pdata;

printf("[IDENTIFY] vid     : 0x%02X", identifyControllerData->VID);

    printf("[IDENTIFY] nn     : 0x%02X", identifyControllerData->NN);

unsigned char str[41];

    memcpy(str, identifyControllerData->SN, 20);

    str[20] = '\0';

    printf("[IDENTIFY] serial_num : %s\r\n", str);

    memcpy(str, identifyControllerData->MN, 40);

    str[40] = '\0';

    printf("[IDENTIFY] model_num  : %s\r\n", str);

    memcpy(str, identifyControllerData->FR, 8);

    str[8] = '\0';

    printf("[IDENTIFY] firmware_rev: %s\r\n", str);

    return 0;

}

或者 Get Log Page:

DWORD nvme_get_log_page(NVME_LOG_PAGES lid)

{

    HANDLE FileHandle = CreateFileA(

        "\\\\.\\physicaldrive0", GENERIC_WRITE | GENERIC_READ,

    FILE_SHARE_READ | FILE_SHARE_WRITE,

    NULL, OPEN_EXISTING, 0, NULL

  );

  CHAR pdata[NVME_MAX_LOG_SIZE];

    if(nvme_specific(FileHandle, NVMeDataTypeLogPage, lid, 0, pdata, NVME_MAX_LOG_SIZE, NULL)) {

        return 1;

    }

  switch(lid)

  {

    case NVME_LOG_PAGE_HEALTH_INFO:

      PNVME_HEALTH_INFO_LOG smartInfo = (PNVME_HEALTH_INFO_LOG)pdata;

      printf("SMART/Health Info - Temperature %d.\n", ((ULONG)smartInfo->Temperature[1] << 8 | smartInfo->Temperature[0]) - 273);

  }

    return 0;

}

或者 Get Feature:

void nvme_fid_callback(DWORD fid, DWORD cdw0)

{

    printf("[GET FEATURE] ");

    switch (fid)

    {

    case NVME_FEATURE_POWER_MANAGEMENT:

        printf("PS=%d\r\n", cdw0);

        break;

                 default:

        printf("CDW0=%d\r\n", cdw0);

        break;

    }

};

DWORD nvme_get_feature(NVME_FEATURES fid)

{

    HANDLE FileHandle = CreateFileA(

        "\\\\.\\physicaldrive0", GENERIC_WRITE | GENERIC_READ,

    FILE_SHARE_READ | FILE_SHARE_WRITE,

    NULL, OPEN_EXISTING, 0, NULL

  );

    return nvme_specific(FileHandle, NVMeDataTypeFeature, fid, 0, NULL, 0, nvme_fid_callback);

}

For VUC Commands

VUC 的話(OPC =0xC0~0xFF)可以透過以下函式呼叫。

/*!

 * For VUC command only, OPC ranges 0xC0-0xFF

 * @param data_type can only be NVMeDataTypeIdentify, NVMeDataTypeFeature, or NVMeDataTypeLogPage

 * @param sqe the standard 64-byte NVMe Submission Queue Entry

 * @param prtc data transfer direction, can be NVME_PROTOCOL_NON_DATA, NVME_PROTOCOL_DATA_IN, or NVME_PROTOCOL_DATA_OUT

 * @param pdata used if additional data is request

 * @param xfer_len size of pdata in bytes

 */

DWORD nvme_vuc(HANDLE FileHandle, PNVME_COMMAND sqe, NVME_PROTOCOLS prtc, PCHAR pdata, DWORD xfer_len)

{

    BOOL result;

    PVOID buffer = NULL;

    ULONG bufferLength = 0;

    ULONG returnedLength = 0;

PSTORAGE_PROTOCOL_COMMAND protocolCommand = NULL;

    PNVME_COMMAND command = NULL;

//

    // Allocate buffer for use.

    //

    bufferLength = sizeof(STORAGE_PROTOCOL_COMMAND) + STORAGE_PROTOCOL_COMMAND_LENGTH_NVME + sizeof(NVME_ERROR_INFO_LOG) + xfer_len;

    buffer = malloc(bufferLength);

if (buffer == NULL)

    {

        printf("DeviceNVMeQueryProtocolDataTest: allocate buffer failed, exit.\n");

        return 0;

    }

ZeroMemory(buffer, bufferLength);

    protocolCommand = (PSTORAGE_PROTOCOL_COMMAND)buffer;

protocolCommand->Version = STORAGE_PROTOCOL_STRUCTURE_VERSION;

    protocolCommand->Length = sizeof(STORAGE_PROTOCOL_COMMAND);

    protocolCommand->ProtocolType = ProtocolTypeNvme;

    protocolCommand->Flags = STORAGE_PROTOCOL_COMMAND_FLAG_ADAPTER_REQUEST;

    protocolCommand->CommandLength = STORAGE_PROTOCOL_COMMAND_LENGTH_NVME;

    protocolCommand->ErrorInfoLength = sizeof(NVME_ERROR_INFO_LOG);

    protocolCommand->TimeOutValue = 10;

    protocolCommand->ErrorInfoOffset = FIELD_OFFSET(STORAGE_PROTOCOL_COMMAND, Command) + STORAGE_PROTOCOL_COMMAND_LENGTH_NVME;

    protocolCommand->CommandSpecific = STORAGE_PROTOCOL_SPECIFIC_NVME_ADMIN_COMMAND;

    if (prtc == NVME_PROTOCOL_DATA_IN)

    {

        protocolCommand->DataFromDeviceTransferLength = xfer_len;

        protocolCommand->DataFromDeviceBufferOffset = protocolCommand->ErrorInfoOffset + protocolCommand->ErrorInfoLength;

    }

    else if (prtc == NVME_PROTOCOL_DATA_OUT)

    {

        protocolCommand->DataToDeviceTransferLength = xfer_len;

        protocolCommand->DataToDeviceBufferOffset = protocolCommand->ErrorInfoOffset + protocolCommand->ErrorInfoLength;

  memcpy_s((PCHAR)buffer + protocolCommand->DataToDeviceBufferOffset, xfer_len, pdata, xfer_len);

    }

    memcpy_s(protocolCommand->Command, STORAGE_PROTOCOL_COMMAND_LENGTH_NVME, sqe, STORAGE_PROTOCOL_COMMAND_LENGTH_NVME);

//

    // Send request down.

    //

    result = DeviceIoControl(FileHandle,

                             IOCTL_STORAGE_PROTOCOL_COMMAND,

                             buffer,

                             bufferLength,

                             buffer,

                             bufferLength,

                             &returnedLength,

                             NULL);

if (protocolCommand->ReturnStatus != STORAGE_PROTOCOL_STATUS_SUCCESS)

    {

        PNVME_ERROR_INFO_LOG err = (PNVME_ERROR_INFO_LOG)((PCHAR)buffer + protocolCommand->ErrorInfoOffset);

        printf("Fail, Return Status=%d, ", protocolCommand->ReturnStatus);

        printf("SCT=0x%02X, SC=0x%02X\n", err->Status.SCT, err->Status.SC);

        return err->Status.AsUshort;

    }

    else

                {

        printf("PASS\r\n");

        if (prtc == NVME_PROTOCOL_DATA_IN)

        {

            memcpy_s(pdata, xfer_len, (PCHAR)buffer + protocolCommand->DataFromDeviceBufferOffset, xfer_len);

        }

    }

return 0;

}

前提是 Controller 要支援 Get Log Page – Commands Supported and Effects 頁且相應的 VUC Opcode 有描述正確。

Opcode 的 Bit0, Bit1 要符合 Spec 規範,否則 Data 傳輸會有問題

然後就可以:

NVME_COMMAND sq = {0};

sq.CDW0.OPC = opc;

sq.u.GENERAL.CDW10 = 0;

sq.u.GENERAL.CDW11 = 0;

sq.u.GENERAL.CDW12 = 0;

sq.u.GENERAL.CDW13 = 0;

sq.u.GENERAL.CDW14 = 0;

sq.u.GENERAL.CDW15 = 0;

nvme_vuc(FileHandle, &sq, (NVME_PROTOCOLS)protocol, NULL, 0);

雖然看起來有點囉嗦(真的是滿囉嗦的),但這樣我們就可以下 NVMe Admin Command 惹,也只有那幾個基本的呵呵。

編譯就懶得用 Makefile 了,重點是還要另外裝啊啊啊,Windows 哎~

g++ main.cpp -o test.exe

其他 NVMe Command

你如果在好奇,想下前面提到的 Identify、Get Log Page、Get Feature、VUC 以外的 Command 怎麼辦?微軟提供了另一種迂迴方式讓你走,SCSI Translation,也就是要你對 NVMe 磁碟機下 SCSI 指令,內建的 Driver 再根據 Spec 幫你轉成對應的 NVMe Command。

提供大家文件參考,就不再贅述具體的實作細節。

• StorNVMe SCSI Translation Support | Microsoft

• NVM Express: SCSI Translation Reference (PDF)

Reference

1. Working with NVMe drives | Microsoft Docs

2. NVM Express: SCSI Translation Reference